﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Windows;
using System.Xml;
using System.Xml.XPath;

namespace YSCommander.Data
{
	/// <summary>
	/// Manages YSCommander data.
	/// </summary>
	public sealed class CommanderData
	{
		#region Constants

		/// <summary>
		/// Current document version.
		/// </summary>
		public const string DataCurrentVersion = "1.0";

		/// <summary>
		/// The last document version.
		/// </summary>
		public const string DataLastVersion = "1.0";

		private const string ElementYsCommander = "YSCommander";
		private const string ElementVersion = "Version";
		private const string ElementSelectedSectionIndex = "SelectedSectionIndex";
		private const string ElementSelectedProgramIndex = "SelectedProgramIndex";
		private const string ElementSection = "Section";

		#endregion Constants

		#region Fields

		/// <summary>
		/// Holds the section list.
		/// \remarks
		/// - The key is section name.
		/// - The value is section.
		/// 
		/// </summary>
		private readonly Dictionary<String, Section> sections_;

		/// <summary>
		/// Holds the application name.
		/// </summary>
		private readonly String applicationName_;

		#endregion Fields
	
		#region Properties
		/// <summary>
		/// Gets the data file name.
		/// </summary>
		/// <returns>the data file name</returns>
		private static String DataFileName
		{
			get
			{
				String path = WorkFolder;

				if (null != path)
				{
					String applicationName = Assembly.GetExecutingAssembly().GetName().Name;
					path += "\\" + applicationName + ".xml";
				}

				return path;
			}
		}
			
		/// <summary>
		/// Gets the work folder.
		/// </summary>
		public static String WorkFolder
		{
			get
			{
				bool error = false;

				var applicationName = Assembly.GetExecutingAssembly().GetName().Name;
				var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
				path += "\\" + applicationName;

				if (!Directory.Exists(path))
				{
					try
					{
						Directory.CreateDirectory(path);
					}

					catch (UnauthorizedAccessException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture, "You have not required permissions to create folder \"{0}\": {1}",
							path, excp.Message), applicationName, MessageBoxButton.OK, MessageBoxImage.Error);
						error = true;
					}

					catch (ArgumentException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture, "The path \"{0}\" is not valid: {1}",
							path, excp.Message), applicationName, MessageBoxButton.OK, MessageBoxImage.Error);
						error = true;
					}

					catch (PathTooLongException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture, "The specified folder \"{0}\" exceed the system-defined maximum length: {1}",
							path, excp.Message), applicationName, MessageBoxButton.OK, MessageBoxImage.Error);
						error = true;
					}

					catch (DirectoryNotFoundException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture, "The path \"{0}\" is not valid: {1}",
							path, excp.Message), applicationName, MessageBoxButton.OK, MessageBoxImage.Error);
						error = true;
					}

					catch (NotSupportedException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture, "The path \"{0}\" contains a colon character (:) that is not part of a drive label (e.g. \"C:\\\"): {1}",
							path, excp.Message), applicationName, MessageBoxButton.OK, MessageBoxImage.Error);
						error = true;
					}

					catch (IOException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture, "Can't create folder \"{0}\". Error: {1}",
							path, excp.Message),
							applicationName, MessageBoxButton.OK, MessageBoxImage.Error);
						error = true;
					}

					if (error)
					{
						path = null;
					}

				}

				return path;
			}
			
		}

		/// <summary>
		/// Gets sorted by name the section collection.
		/// </summary>
		/// <returns>Sorted by name the section list.</returns>
		public ReadOnlyCollection<Section> Sections
		{
			get
			{
				var sectionList = new List<Section>();

				IEnumerable<Section> query = sections_.Values.OrderBy(section => section.SectionName);

				foreach (var section in query)
				{
					sectionList.Add(section);
				}

				var sectionCollection = new ReadOnlyCollection<Section>(sectionList);

				return sectionCollection;
			}
		}

		/// <summary>
		/// Gets or sets the selected section index.
		/// </summary>
		public int SelectedSectionIndex
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the selected section index.
		/// </summary>
		public int SelectedProgramIndex
		{
			get;
			set;
		}
		#endregion Properties

		#region Methods

		/// <summary>
		/// Creates the instance.
		/// </summary>
		public CommanderData()
		{
			sections_ = new Dictionary<string, Section>();
			applicationName_ = Assembly.GetExecutingAssembly().GetName().Name;
			SelectedSectionIndex = 0;
			SelectedProgramIndex = 0;
		}

		/// <summary>
		/// Gets sorted by name the program collection for specified section..
		/// </summary>
		/// <param name="sectionName">Specifies the section.</param>
		/// <returns>Sorted by name the program list.</returns>
		public ReadOnlyCollection<Program> GetPrograms(String sectionName)
		{
			var section = sections_[sectionName];

			return null != section ? section.Programs : new ReadOnlyCollection<Program>(new List<Program>());
		}

		/// <summary>
		/// Gets the section by name.
		/// </summary>
		/// <param name="sectionName">Specifies the section name.</param>
		/// <returns>The section by name.</returns>
		public Section GetSection(String sectionName)
		{
			return sections_[sectionName];
		}

		/// <summary>
		/// Deletes the section.
		/// </summary>
		/// <param name="section">Specifies the section name to delete.</param>
		public void DeleteSection(Section section)
		{
			if (sections_.ContainsKey(section.SectionName))
			{
				sections_.Remove(section.SectionName);
			}
		}

		/// <summary>
		/// Deletes the program.
		/// </summary>
		/// <param name="section">Specifies the section.</param>
		/// <param name="program">Specifies the program to delete.</param>
		public static void DeleteProgram(Section section, Program program)
		{
			if (section.IsProgramExists(program.ProgramName))
			{
				section.DeleteProgram(program);
			}
		}

		/// <summary>
		/// Adds a section to section dictionary.
		/// </summary>
		/// <param name="section">New section to add.</param>
		/// <returns>True, if the section was added successfully; otherwise returns false - section with same name already exists.</returns>
		public bool AddSection(Section section)
		{
			var wasAdded = false;

			if (!sections_.ContainsKey(section.SectionName))
			{
				sections_[section.SectionName] = section;
				wasAdded = true;
			}

			return wasAdded;
		}

		/// <summary>
		/// Adds a program to specified section.
		/// </summary>
		/// <param name="section">Existing section where to add the program.</param>
		/// <param name="program">New program to add.</param>
		/// <returns>True, if the program was added successfully; otherwise returns false - section doesn't exists or program with same name exists already.</returns>
		public bool AddProgram(Section section, Program program)
		{
			var wasAdded = IsSectionExists(section.SectionName);

			if (wasAdded)
			{
				wasAdded = !sections_[section.SectionName].IsProgramExists(program.ProgramName) && sections_[section.SectionName].AddProgram(program);
			}

			return wasAdded;
		}

		/// <summary>
		/// Loads the commander data from the XML file.
		/// </summary>
		public void LoadData()
		{
			sections_.Clear();

			var dataFileName = DataFileName;

			if (null != dataFileName)
			{
				if (File.Exists(dataFileName))
				{
					try
					{
						var doc = new XmlDocument();
						doc.Load(dataFileName);

						SelectedSectionIndex = GetIntValue(doc, ElementSelectedSectionIndex);
						SelectedProgramIndex = GetIntValue(doc, ElementSelectedProgramIndex);

						foreach (XmlNode nSection in doc.DocumentElement.SelectNodes("//" + ElementSection))
						{
							var section = new Section();
							section.Deserialize(nSection);
							AddSection(section);
						}

					}
					catch (XmlException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "XML Document \"{0}\" is corrupted {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (ArgumentException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect document name \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (PathTooLongException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect document name \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (DirectoryNotFoundException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect document name \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (UnauthorizedAccessException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect file \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (FileNotFoundException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect document name \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (NotSupportedException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect document name \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (SecurityException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect file \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
					catch (IOException excp)
					{
						MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						                              "Incorrect file \"{0}\": {1}", dataFileName, excp.Message),
						                applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
					}
				}
			}
		}

		/// <summary>
		/// Stores the commander data into the XML file.
		/// </summary>
		public void SaveData()
		{
			// Serialization
			String dataFileName = DataFileName;

			if (null != dataFileName)
			{
				try
				{
					var doc = new XmlDocument();
					var root = doc.CreateElement(ElementYsCommander);
					doc.AppendChild(root);

					var version = doc.CreateElement(ElementVersion);
					version.InnerText = DataCurrentVersion;
					root.AppendChild(version);

					var selectedSectionIndex = doc.CreateElement(ElementSelectedSectionIndex);
					selectedSectionIndex.InnerText = SelectedSectionIndex.ToString();
					root.AppendChild(selectedSectionIndex);

					var selectedProgramIndex = doc.CreateElement(ElementSelectedProgramIndex);
					selectedProgramIndex.InnerText = SelectedProgramIndex.ToString();
					root.AppendChild(selectedProgramIndex);

					foreach (var section in sections_.Values)
					{
						var child = section.Serialize(doc) as XmlNode;
						if (child != null)
						{
							root.AppendChild(child);
						}
					}

					var settings = new XmlWriterSettings
					               	{
										Indent = true,
                                        IndentChars = ("    ")
					               	};
					
					using (var writer = XmlWriter.Create(dataFileName, settings))
					{
						if (writer != null)
						{
							// Write XML data.
							doc.Save(writer);
							writer.Flush();
						}
					}
				}
				catch (XmlException excp)
				{
					MessageBox.Show(String.Format(CultureInfo.InvariantCulture,
						"Can't save data: {0}", excp.Message),
						applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
				}
			}
		}

		/// <summary>
		/// Checks, if the section with specified name exists.
		/// </summary>
		/// <param name="sectionName">Specifies the section name to check.</param>
		/// <returns>True, if the section with specified name exists.</returns>
		public bool IsSectionExists(String sectionName)
		{
			return sections_.ContainsKey(sectionName);
		}

		/// <summary>
		/// Checks, if the program with specified name exists in specified setion.
		/// </summary>
		/// <param name="sectionName">Specifies the section name to check.</param>
		/// <param name="programName">Specifies the program name to check.</param>
		/// <returns>True, if the program with specified name exists in specified setion.</returns>
		public bool IsProgramExists(String sectionName, String programName)
		{
			var exists = sections_.ContainsKey(sectionName);
			if (exists)
			{
				var section = sections_[sectionName];

				exists = section.IsProgramExists(programName);
			}
			return exists;
		}

		/// <summary>
		/// Copies the program with new name.
		/// </summary>
		/// <param name="section">Section.</param>
		/// <param name="program">Program to copy.</param>
		/// <param name="newName">New program name.</param>
		/// <returns></returns>
		public bool CopyProgram(Section section, Program program, String newName)
		{
			var rslt = !section.IsProgramExists(newName);

			if (rslt)
			{
				var newProgram = new Program(program)
				                 	{
										ProgramName = newName
				                 	};

				rslt = AddProgram(section, newProgram);
			}

			return rslt;
		}
		
		/// <summary>
		/// Executes the program.
		/// </summary>
		/// <param name="program">Program to execute.</param>
		/// <returns>True, if the program was started successfully.</returns>
		public bool ExecuteProgram(Program program)
		{
			bool rslt;

			var startInfo = new System.Diagnostics.ProcessStartInfo(program.Location, program.Parameters);
			if (!program.IsOpenWindow)
			{
				startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
			}

			if (0 < program.StartFolder.Length)
			{
				startInfo.WorkingDirectory = program.StartFolder;
			}

			try
			{
				var p = System.Diagnostics.Process.Start(startInfo);
				rslt = (null != p);
			}
			catch (InvalidOperationException excp)
			{
				MessageBox.Show(String.Format(CultureInfo.InvariantCulture, 
					"Can't execute application: {0}", excp.Message), 
					applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
				rslt = false;
			}
			catch (Win32Exception excp)
			{

				MessageBox.Show(String.Format(CultureInfo.InvariantCulture, 
					"Can't execute application: {0}", excp.Message), 
					applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
				rslt = false;
			}

			return rslt;
		}

		/// <summary>
		/// Executes the script.
		/// </summary>
		/// <param name="program">Script to execute.</param>
		/// <returns>True, if the script was started successfully.</returns>
		public bool ExecuteScript(Program program)
		{
			bool rslt;
			var fileName = Path.GetRandomFileName() + ".bat";
			var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
			path += "\\" + applicationName_ + "\\" + fileName;

			var sw = new StreamWriter(path);
			sw.Write(program.Script);
			sw.Close();

			var startInfo =
				new System.Diagnostics.ProcessStartInfo(path, program.Parameters);
			if (!program.IsOpenWindow)
			{
				startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
			}

			if (0 < program.StartFolder.Length)
			{
				startInfo.WorkingDirectory = program.StartFolder;
			}

			try
			{
				System.Diagnostics.Process p = System.Diagnostics.Process.Start(startInfo);
				rslt = (null != p);
			}
			catch (InvalidOperationException excp)
			{
				MessageBox.Show(String.Format(CultureInfo.InvariantCulture, 
					"Can't execute application: {0}", excp.Message), 
					applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
				rslt = false;
			}
			catch (Win32Exception excp)
			{

				MessageBox.Show(String.Format(CultureInfo.InvariantCulture, 
					"Can't execute application: {0}", excp.Message), 
					applicationName_, MessageBoxButton.OK, MessageBoxImage.Error);
				rslt = false;
			}

			return rslt;
		}

		/// <summary>
		/// Removes all temporary created files with scripts.
		/// </summary>
		public static void ClearWorkFolder()
		{
			var path = WorkFolder;

			if (null != path)
			{
				foreach (var fileName in Directory.GetFiles(path, "*.bat"))
				{
					try
					{
						File.Delete(fileName);
					}
					catch (ArgumentException)
					{
					}
					catch (DirectoryNotFoundException)
					{
					}
					catch (NotSupportedException)
					{
					}
					catch (PathTooLongException)
					{
					}
					catch (UnauthorizedAccessException)
					{
					}
					catch (IOException)
					{
					}
				}
			}
		}

		/// <summary>
		/// Extracts the int value from the specified elemnt.
		/// </summary>
		/// <param name="doc"></param>
		/// <param name="elementName"></param>
		/// <returns></returns>
		private static int GetIntValue(IXPathNavigable doc, String elementName)
		{
			var rslt = 0;

			var xmlDocument = doc as XmlDocument;

			var selectedSectionIndex = xmlDocument.DocumentElement.SelectSingleNode("//" + elementName);
			
			if (null != selectedSectionIndex)
			{
				var isOk = true;
				try
				{
					rslt = Int32.Parse(selectedSectionIndex.InnerText);
				}
				catch (FormatException)
				{
					isOk = false;
				}
				catch (OverflowException)
				{
					isOk = false;
				}

				if (!isOk)
				{
					rslt = 0;
				}
			}
			return rslt;
		}

		#endregion Methods
	}
}
